Skip to content

feat(ENG-13683): add Maven credential helper via shell plugin - #339

Draft
cloudsmith-iduffy wants to merge 1 commit into
masterfrom
maven-shell-plugin
Draft

feat(ENG-13683): add Maven credential helper via shell plugin#339
cloudsmith-iduffy wants to merge 1 commit into
masterfrom
maven-shell-plugin

Conversation

@cloudsmith-iduffy

@cloudsmith-iduffy cloudsmith-iduffy commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Description

The last of the stack, rebased onto master now that #336, #337 and #338 have
landed, and squashed to a single commit.

Maven has no credential-helper protocol, so this authenticates it with a shell
plugin instead. credential-helper install maven --org <org> --repo <repo>
writes an mvn shim that wraps every invocation in cloudsmith exec, which
injects an ephemeral mode-0600 settings.xml via mvn -s and deletes it when
the run ends — no token is ever written to durable configuration.

  • credential-helper shell-init prints the shell initialisation (bash, zsh,
    fish) that puts the shims directory first on PATH.
  • cloudsmith exec -- <command> is callable directly, for CI that should not
    touch PATH.
  • Publishing is opt-in: install prints the distributionManagement snippet to
    add to pom.xml.
  • Custom download/upload domains are discovered from the organisation. A domain
    bound to the repository being installed beats an organisation-wide one, and
    one bound to a different repository is never used. Which kind each host is
    gets recorded in package-managers.ini, so wrapped runs need no lookup — the
    run path makes no API call and no cache read.

Credential binding. Maven matches a <server> to a repository by id alone,
with no host check, and the shim applies to every mvn run in every directory.
A fixed, guessable server id would therefore let any checked-out pom.xml
declaring a repository under that id receive the token on an ordinary
mvn compile. So a wrapped run binds the injected download credential to a
random id generated fresh per invocation, and the stable id your
distributionManagement names is supplied only when the invocation is actually
a deploy.

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation update
  • Refactoring
  • Other (please describe)

Additional Notes

Test suite: 783 passed, 40 skipped at this commit.

Verified end to end in GitHub Actions with OIDC as the only credential
(run):
a plain mvn -B clean deploy resolved a private dependency through the
organisation's discovered custom domain
(dl-prod.iduffy.cloudsmith.sh/basic/default/maven/ — no org segment, as a
custom domain identifies it) and published the built jar to
maven.cloudsmith.io/iduffy-demo/default/.

Two things a reviewer should know:

  • Wrapped runs do not consult ~/.m2/settings.xml; mirrors, proxies and other
    <server> entries are not seen. Passing your own -s/--settings runs Maven
    unwrapped, with a warning. This is the designed mechanism — Maven has no
    settings-merge — and is called out at install time and in the CHANGELOG.
  • Two known limitations ship knowingly. The ephemeral settings.xml is removed
    in a finally, so a SIGKILL/SIGTERM of cloudsmith exec (a cancelled CI step)
    leaves the 0600 token file in /tmp until the agent is recycled; a signal
    handler is the fix and wants its own change. And the residual exposure below.
  • One residual exposure ships knowingly: the stable server id defaults to the
    literal cloudsmith, so on a deploy a hostile checkout can still claim it
    during dependency resolution. Minting it as cloudsmith-<random> at install
    time would close it; that changes a default users paste into pom.xml, so it
    is deliberately left to its own change. Documented in build_settings_xml's
    docstring.

Comment thread cloudsmith_cli/cli/tests/commands/test_credential_helper_maven_installer.py Dismissed
@cloudsmith-iduffy cloudsmith-iduffy changed the title maven shell plugin feat(ENG-13683): add Maven credential helper via shell plugin Aug 1, 2026
@BartoszBlizniak
BartoszBlizniak changed the base branch from credential-generic to custom-domains August 3, 2026 11:45
cloudsmith-iduffy added a commit that referenced this pull request Aug 4, 2026
This branch shipped an API surface nothing in it calls. Every consumer is in
the Maven helper (#339), which is stacked on top of this branch, so the code
and its tests move there, where the first caller lives and where a reviewer
can see what they are for.

Moved: select_custom_domain and CustomDomain.serves_repository; the
single-host resolvers default_host, default_host_for_type, builtin_host and
builtin_host_for_type with their two private helpers; and domain_scope.
With them go the twelve select_custom_domain tests, the six host-resolver
tests and the persisted-scope test.

Staying: _precedence_key and the CustomDomain.scope / is_bound_to pair it
reads, because get_format_domains sorts by it and the Docker installer and
runtime helper both call that. The DomainScope enum stays with them; only
its string parser moves.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Base automatically changed from custom-domains to master August 9, 2026 01:15
Maven has no credential-helper protocol, so there is nothing to install a
launcher for. This adds a shell plugin instead: an `mvn` shim that wraps every
invocation in `cloudsmith exec`, which provisions credentials for that single
run and cleans them up afterwards.

`cloudsmith credential-helper install maven --org <org> --repo <repo>` writes
the shim into the Cloudsmith shims directory and records the binding (org,
repo, resolved download/upload hosts, registry id) in `package-managers.ini`.
`credential-helper shell-init` emits the bash/zsh/fish snippet that puts the
shims directory first on PATH.

A wrapped run resolves dependencies through an ephemeral, mode-0600
`settings.xml` injected via `mvn -s` and deleted when the run ends, so no token
is ever written to durable configuration. Maven matches a `<server>` to a
repository by id alone, with no host check, so the injected download credential
is bound to a random id minted per invocation; the stable id named in
`distributionManagement` is supplied only when the invocation is actually a
deploy. Publishing stays opt-in: `install` prints the snippet to paste into
`pom.xml`. A user-supplied `-s/--settings` runs Maven unwrapped, with a warning,
rather than silently shadowing their file.

Custom domains are discovered as for the Docker helper and ranked the way the
server ranks overlapping domains, so the binding records the host Cloudsmith
would itself serve. A repository-scoped domain identifies the repository on its
own, so its URLs carry neither org nor repo segment; because it serves only the
repository it was discovered for, `cloudsmith exec --repo <other>` resets it to
the default host for that invocation.

Alongside it:

- `cloudsmith exec -- <command>` is callable directly, for CI that would rather
  not touch PATH. The package manager is detected from the command name;
  `--org`/`--repo` flags override the stored binding, while the same values
  from the environment or config.ini apply only when nothing is stored, since
  OIDC users keep CLOUDSMITH_ORG exported permanently.
- `default_domains` gains host resolution by backend kind and by domain type,
  honouring a trusted `[domains]` override, plus `DomainScope` for what a
  discovered domain is bound to; `common` gains the URL path-segment rule that
  follows from it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
entry = config.get_plugin("maven")
assert entry.cdn_host == "dl.cloudsmith.io"
assert entry.cdn_scope == "organization"
assert any(a.startswith("WARNING") and "dl-staging.acme.com" in a for a in actions)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants